package com.katesoft.scale4j.persistent.model.unified;

import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.persistent.utils.HibernateReflectionUtility;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.hibernate.envers.Audited;
import org.hibernate.proxy.HibernateProxy;
import org.hibernate.proxy.LazyInitializer;
import org.hibernate.search.annotations.DocumentId;
import org.springframework.util.ReflectionUtils.FieldCallback;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
import java.lang.reflect.Field;
import java.util.UUID;

import static org.springframework.util.ReflectionUtils.COPYABLE_FIELDS;
import static org.springframework.util.ReflectionUtils.doWithFields;
import static org.springframework.util.ReflectionUtils.makeAccessible;

/**
 * Default implementation of IBO interface.
 * <p/>
 * Clients should extend this class.
 * <p/>
 * default hibernate access type is field access
 *
 * @author kate2007
 * @see AbstractPersistentEntity
 */
@MappedSuperclass
@Access(AccessType.FIELD)
@Audited
public abstract class BO implements IBO, Cloneable
{
    private static final Logger LOGGER = LogFactory.getLogger(BO.class);
    //
    public static final String PROP_GUID = "guid";
    public static final String PROP_UID = "uniqueIdentifier";
    public static final String PROP_VERSION = "version";
    //================================================================
    @Column(name = "global_unique_identifier",
            nullable = false,
            updatable = false,
            unique = true)
    private String guid;
    @DocumentId
    @Id
    @Column(name = "unique_identifier",
            nullable = false,
            updatable = false,
            unique = true)
    private Long uniqueIdentifier;
    //
    @SuppressWarnings({"UnusedDeclaration"})
    @Version
    @Column(name = "version",
            nullable = false,
            updatable = true)
    private Long version;
    //================================================================

    public BO()
    {
        guid = UUID.randomUUID().toString();
        version = (long) 0;
    }

    @Override
    public Class<? extends BO> getPersistentClass() {return getClass();}

    @Override
    public String getGlobalUniqueIdentifier() {return guid;}

    @Override
    public Long getUniqueIdentifier() {return uniqueIdentifier;}

    @Override
    public long getVersion() {return version == null ? 0L : version;}

    /**
     * internal setter, clients should not use this method directly.
     *
     * @param version new version
     */
    protected void setVersion(long version)
    {
        this.version = version;
    }

    /**
     * internal setter, clients should not used this method directy.
     * @param guid newValue
     */
    protected void setGlobalUniqueIdentifier(String guid)
    {
        this.guid = guid;
    }

    @Override
    public void cleanBeanProperties()
    {
        LOGGER.trace("cleaning bean properties, entity = %s", toString());
        doWithFields(getPersistentClass(), new FieldCallback()
        {
            @Override
            public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException
            {
                if (HibernateReflectionUtility.isPersistentField(field)) {
                    LOGGER.trace("cleaning %s.%s", getPersistentClass().getName(), field.getName());
                    makeAccessible(field);
                    field.set(BO.this, null);
                }
            }
        }, COPYABLE_FIELDS);
    }

    @Override
    public boolean attributesLoaded()
    {
        if (this instanceof HibernateProxy) {
            final LazyInitializer hibernateLazyInitializer = ((HibernateProxy) this).getHibernateLazyInitializer();
            return !hibernateLazyInitializer.isUninitialized();
        }
        return true;
    }

    /** this method will be executed in hibernate session, never outside session. */
    @Override
    public void forceAttributesLoad()
    {
        if (this instanceof HibernateProxy) {
            LOGGER.debug("forced hibernate lazy loading for entity = %s", toString());
            LazyInitializer initializer = ((HibernateProxy) this).getHibernateLazyInitializer();
            initializer.initialize();
        }
    }

    /**
     * set unique identifier of entity. NO need to populate this field manually - there is already distributed id generator that will populate this field.
     *
     * @param uniqueIdentifier newValue
     */
    public void setUniqueIdentifier(long uniqueIdentifier)
    {
        this.uniqueIdentifier = uniqueIdentifier;
    }

    @Override
    public String reflectionToString()
    {
        return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE, false);
    }

    /**
     * hashCode is generated using global_unique_identifier, unique_identifier, and version fields.
     *
     * @return hash of this object.
     */
    @Override
    public int hashCode()
    {
        HashCodeBuilder builder = new HashCodeBuilder();
        //
        builder.append(getUniqueIdentifier());
        builder.append(getVersion());
        builder.append(getGlobalUniqueIdentifier());
        //
        return builder.toHashCode();
    }

    /**
     * by default objects are equals if global_unique_identifier, unique_identifier, and version match.
     *
     * @param obj another object of the same class
     * @return true if equals by global_unique_identifier, unique_identifier, and version.
     */
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj) {return true;}
        if (obj == null || getClass() != obj.getClass()) {return false;}
        BO other = (BO) obj;
        EqualsBuilder builder = new EqualsBuilder();
        //
        builder.append(getUniqueIdentifier(), other.getUniqueIdentifier());
        builder.append(getVersion(), other.getVersion());
        builder.append(getGlobalUniqueIdentifier(), other.getGlobalUniqueIdentifier());
        //
        return builder.isEquals();
    }

    @SuppressWarnings({"CloneDoesntDeclareCloneNotSupportedException"})
    @Override
    public BO clone()
    {
        try {
            LOGGER.debug("cloning this %s", this.toString());
            return (BO) super.clone();
        }
        catch (final CloneNotSupportedException e) {
            LOGGER.error(e);
            throw new RuntimeException(e);
        }
    }

    /**
     * This method will return short representation of this entity including class, global_unique_identifier information.
     * <p/>
     * NOTE: toString() does not include unique_identifier and version because it can cause session flushing before transaction commit if somebody will try
     * to
     * use unique identifier.
     *
     * @return entity representation in format [class_name:/global_unique_identifier]
     */
    @Override
    public String toString()
    {
        return String.format("%s:/%s", getPersistentClass().getName(), getGlobalUniqueIdentifier());
    }

    /**
     * by default nothing should prevent us from removing from cache.
     * <p/>
     * subclasses must add more useful logic(for example they expect that this entity will not be used for next 12 h,
     * in this case they can remove this entity
     * from cache).
     * <p/>
     * From the other hand it is possible that clients are expecting that this entity will be request a lot of time in nearest future(in this case they would
     * like to leave this entity in cache).
     *
     * @return true
     */
    @Override
    public boolean isCleanable()
    {
        return true;
    }
}
